#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <limits.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <assert.h>
#include <inttypes.h>

#if defined(__APPLE__) && defined(__MACH__)
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif

#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/mman.h>

struct pixel {
	float r, g, b;
	unsigned int unused1;
	void *unused2;
};

struct framebuffer {
	int width, height;
	struct pixel pix[1];
};

int attach_rend(int pid);
int find_rend_pid(void);
void disp(void);
int read_image(int *width, int *height);
void create_texture(int width, int height);
void upd_scanline(int s);
void upd_image(void);
void reshape(int x, int y);
void keyb(unsigned char key, int x, int y);
void sighandler(int sig);

int progress;
struct framebuffer *fb;
int attach_wait = 2;
int lis_pid;

int main(int argc, char **argv)
{
	int i, xsz, ysz, pid = -1;
	char *shmpath = 0;

	glutInit(&argc, argv);

	for(i=1; i<argc; i++) {
		if(argv[i][0] == '-' && argv[i][2] == 0) {
			switch(argv[i][1]) {
			case 'p':
				if(argv[i + 1] && isdigit(argv[i + 1][0])) {
					pid = atoi(argv[++i]);
				}
				progress = 1;
				break;

			case 'm':
				shmpath = argv[++i];
				break;

			case 'w':
				if(!isdigit(argv[++i][0])) {
					fprintf(stderr, "-w must be followed by the number of seconds to wait\n");
					return 1;
				}
				attach_wait = atoi(argv[i]);
				break;

			default:
				fprintf(stderr, "unrecognized argument: %s\n", argv[i]);
				return 1;
			}
		} else {
			static int fcount;

			if(fcount++) {
				fprintf(stderr, "too many files\n");
				return 1;
			}

			close(0);
			if(open(argv[i], O_RDONLY) == -1) {
				fprintf(stderr, "failed to open file: %s: %s\n", argv[i], strerror(errno));
				return 1;
			}
		}
	}

	glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
	glutInitWindowSize(128, 128);
	glutCreateWindow("rendy output viewer");

	if(progress) {
		if(attach_rend(pid) == -1) {
			fprintf(stderr, "failed to attach to the renderer process\n");
			return 1;
		}
		xsz = fb->width;
		ysz = fb->height;
		create_texture(xsz, ysz);
	} else {
		if(read_image(&xsz, &ysz) == -1) {
			return 1;
		}
		fb->width = xsz;
		fb->height = ysz;
		create_texture(xsz, ysz);
		upd_image();
	}

	glutReshapeWindow(xsz, ysz);
	glutDisplayFunc(disp);
	glutReshapeFunc(reshape);
	glutKeyboardFunc(keyb);

	glEnable(GL_TEXTURE_2D);

	glutMainLoop();
	return 0;
}

int attach_rend(int pid)
{
	int s, fd, xsz, ysz, fbsz;
	char shmname[64];
	struct sockaddr_un addr;
	struct sigaction sa;
	time_t wait_start;

	if(pid == -1) {
		wait_start = time(0);

		while((pid = find_rend_pid()) == -1) {
			if(time(0) - wait_start > attach_wait) {
				fprintf(stderr, "could not find renderer process\n");
				return -1;
			}
			usleep(2500);
		}
	}

	/* map the framebuffer */
	sprintf(shmname, "/rendy.%d.shm", pid);
	if((fd = shm_open(shmname, O_RDONLY, 0)) == -1) {
		fprintf(stderr, "failed to open shared memory: %s: %s\n", shmname, strerror(errno));
		return -1;
	}

	if((fb = mmap(0, sizeof *fb, PROT_READ, MAP_SHARED, fd, 0)) == (void*)-1) {
		perror("mmap failed");
		close(fd);
		return -1;
	}
	xsz = fb->width;
	ysz = fb->height;
	munmap(fb, sizeof *fb);

	fbsz = sizeof *fb + xsz * ysz * sizeof(struct pixel);
	if((fb = mmap(0, fbsz, PROT_READ, MAP_SHARED, fd, 0)) == (void*)-1) {
		perror("failed to map shared framebuffer");
		close(fd);
		return -1;
	}
	close(fd);

	/* connect to the progress reporting socket */
	if((s = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
		munmap(fb, fbsz);
		return -1;
	}

	memset(&addr, 0, sizeof addr);
	addr.sun_family = AF_UNIX;
	sprintf(addr.sun_path, "/tmp/rendy.%d.sock", pid);

	if(connect(s, (struct sockaddr*)&addr, sizeof addr) == -1) {
		perror("failed to connect to the render progress socket");
		munmap(fb, fbsz);
		close(s);
		return -1;
	}

	memset(&sa, 0, sizeof sa);
	sa.sa_handler = sighandler;

	/* fork a progress listener and return */
	if((lis_pid = fork())) {
		close(s);
		sigaction(SIGUSR1, &sa, 0);
		sigaction(SIGCHLD, &sa, 0);
	} else {
		sigaction(SIGTERM, &sa, 0);

		for(;;) {
			uint16_t buf[2];

			if(read(s, buf, sizeof buf) < sizeof buf) {
				if(errno == EINTR) {
					continue;
				}
				putchar('\n');
				lis_pid = 0;
				_exit(1);
			}

			printf("progress: %d%%\r", 100 * (int)buf[1] / USHRT_MAX);
			fflush(stdout);
			kill(getppid(), SIGUSR1);
		}
	}

	return 0;
}

int find_rend_pid(void)
{
	int pid = -1;
	DIR *dir;
	struct dirent *dp;

	if(!(dir = opendir("/tmp"))) {
		perror("find_rend_pid failed to open /tmp");
		return -1;
	}

	while((dp = readdir(dir))) {
		if(sscanf(dp->d_name, "rendy.%d.sock", &pid) == 1) {
			printf("FOUND: %s -> %d\n", dp->d_name, pid);
			break;
		}
	}
	closedir(dir);
	return pid;
}

void disp(void)
{
	upd_image();

	glBegin(GL_QUADS);
	glTexCoord2f(0, 0); glVertex2f(-1, 1);
	glTexCoord2f(1, 0); glVertex2f(1, 1);
	glTexCoord2f(1, 1); glVertex2f(1, -1);
	glTexCoord2f(0, 1); glVertex2f(-1, -1);
	glEnd();

	glutSwapBuffers();
}

#define BOM		0xaabb
int read_image(int *width, int *height)
{
	uint16_t bom;
	int xmin, ymin, xsz, ysz;
	ssize_t rd = 0;
	int i, j;

	if(fread(&bom, sizeof bom, 1, stdin) < 1) {
		return -1;
	}
	assert(bom == BOM);

	rd = fread(&xmin, sizeof xmin, 1, stdin);
	rd += fread(&ymin, sizeof ymin, 1, stdin);
	rd += fread(&xsz, sizeof xsz, 1, stdin);
	rd += fread(&ysz, sizeof ysz, 1, stdin);
	if(rd != 4) {
		return -1;
	}

	*width = xsz - xmin;
	*height = ysz - ymin;

	if(!(fb = malloc(sizeof *fb + *width * *height * sizeof(struct pixel)))) {
		return -1;
	}

	for(i=ymin; i<ysz; i++) {
		for(j=xmin; j<xsz; j++) {
			struct pixel *pix = fb->pix + i * *width + j;

			if(fread(&pix->r, sizeof(float), 3, stdin) != 3) {
				free(fb);
				return -1;
			}
		}
	}
	return 0;
}

void create_texture(int width, int height)
{
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexImage2D(GL_TEXTURE_2D, 0, 4, width, height, 0, GL_RGB, GL_FLOAT, 0);
}

void upd_scanline(int s)
{
	static float *scanline;
	int i;
	struct pixel *spix;

	if(!scanline) {
		if(!(scanline = malloc(3 * fb->width * sizeof *scanline))) {
			perror("malloc failed");
			exit(1);
		}
	}

	spix = fb->pix + s * fb->width;
	for(i=0; i<fb->width; i++) {
		scanline[i * 3] = spix[i].r;
		scanline[i * 3 + 1] = spix[i].g;
		scanline[i * 3 + 2] = spix[i].b;
	}

	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, s, fb->width, 1, GL_RGB, GL_FLOAT, scanline);
}

void upd_image(void)
{
	int i;

	for(i=0; i<fb->height; i++) {
		upd_scanline(i);
	}
}

void reshape(int x, int y)
{
	glViewport(0, 0, x, y);
}

void keyb(unsigned char key, int x, int y)
{
	if(key == 27) {
		if(lis_pid) {
			kill(lis_pid, SIGTERM);
		}
		exit(0);
	}
}

void sighandler(int sig)
{
	switch(sig) {
	case SIGCHLD:
		wait(0);
		break;

	case SIGUSR1:
		glutPostRedisplay();
		break;

	case SIGTERM:
		_exit(0);
		break;
	}
}
